package com.xy.etl.opc;


import com.xy.etl.opc.key.OpcKeyStoreLoader;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
import org.springframework.util.StringUtils;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * @author TARZAN
 */
@Slf4j
public class OpcUaUtil {

    /**
     * 方法描述: 创建客户端
     *
     * @param endPointUrl
     * @return {@link OpcUaClient}
     * @throws
     */
    public static OpcUaClient createClient(String endPointUrl) throws Exception {
        return createClient(endPointUrl, null, null);
    }

    /**
     * 方法描述: 创建客户端
     *
     * @param endPointUrl
     * @param username
     * @param password
     * @return {@link OpcUaClient}
     * @throws
     */
    public static OpcUaClient createClient(String endPointUrl, String username, String password) throws Exception {
        Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
        Files.createDirectories(securityTempDir);
        if (!Files.exists(securityTempDir)) {
            throw new Exception("unable to create security dir: " + securityTempDir);
        }

        OpcKeyStoreLoader loader = new OpcKeyStoreLoader().load(securityTempDir);
        //获取安全策略
        List<EndpointDescription> endpointDescription = DiscoveryClient.getEndpoints(endPointUrl).get();
        //过滤出一个自己需要的安全策略
        EndpointDescription endpoint = endpointDescription.stream()
                .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
                .findFirst().orElse(null);
        IdentityProvider identityProvider = new AnonymousProvider();
        if (!StringUtils.isEmpty(username) || !StringUtils.isEmpty(password)) {
            identityProvider = new UsernameProvider(username, password);
        }
        // 设置配置信息
        OpcUaClientConfig config = OpcUaClientConfig.builder()
                // opc ua 自定义的名称
                .setApplicationName(LocalizedText.english("T1"))
                // 地址
                .setApplicationUri("urn:eclipse:milo:examples:client")
                .setCertificate(loader.getClientCertificate())
                .setKeyPair(loader.getClientKeyPair())
                // 安全策略等配置
                .setEndpoint(endpoint)
                .setIdentityProvider(identityProvider)
                //等待时间
                .setRequestTimeout(UInteger.valueOf(5000))
                .build();
        // 准备连接
        OpcUaClient opcClient = OpcUaClient.create(config);
        //开启连接
        opcClient.connect().get();
        log.info("连接成功。。。success");
        return opcClient;

    }

    /**
     * 方法描述: 查询所有节点
     *
     * @param namespaceIndex 命名空间
     * @param client
     * @return
     * @throws Exception
     */
    public static Set<String> getAllKeys(Integer namespaceIndex, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        return browse(namespaceIndex, null, client);
    }


    /**
     * 方法描述: 查询某个节点下的所有节点
     *
     * @param namespaceIndex 命名空间
     * @param identifier
     * @param client
     * @return
     * @throws
     */
    public static Set<String> browse(Integer namespaceIndex, String identifier, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        Set<String> keys = new HashSet<>(500);
        browse(namespaceIndex, identifier, keys, client);
        return keys;
    }

    /**
     * 方法描述: 查询某个节点下的所有节点
     *
     * @param namespaceIndex 命名空间
     * @param identifier
     * @param keys
     * @param client
     * @return
     * @throws Exception
     */
    private static Set<String> browse(Integer namespaceIndex, String identifier, Set<String> keys, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        NodeId nodeId = Identifiers.ObjectsFolder;
        if (!StringUtils.isEmpty(identifier)) {
            nodeId = new NodeId(namespaceIndex, identifier);
        }
        BrowseDescription browse = new BrowseDescription(
                nodeId,
                BrowseDirection.Forward,
                Identifiers.References,
                true,
                UInteger.valueOf(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
                UInteger.valueOf(BrowseResultMask.All.getValue())
        );
        BrowseResult browseResult = client.browse(browse).get();
        ReferenceDescription[] references = browseResult.getReferences();
        for (ReferenceDescription reference : references) {
            System.out.println(reference.getNodeId().getIdentifier().toString() + " " + reference.getNodeClass().getValue());
            keys.add(identifier);
            if (reference.getNodeClass().getValue() == NodeClass.Object.getValue()) {
                browse(namespaceIndex, reference.getNodeId().getIdentifier().toString(), keys, client);
            }
        }
        return keys;
    }


    /**
     * 方法描述: 查询某个节点下的所有节点
     *
     * @param namespaceIndex 命名空间
     * @param identifierPre
     * @param keys
     * @param client
     * @return
     * @throws Exception
     */
    public static Set<String> filter(Integer namespaceIndex, String identifierPre, Set<String> keys, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        NodeId nodeId = Identifiers.ObjectsFolder;
        if (!StringUtils.isEmpty(identifierPre)) {
            nodeId = new NodeId(namespaceIndex, identifierPre);
        }
        BrowseDescription browse = new BrowseDescription(
                nodeId,
                BrowseDirection.Forward,
                Identifiers.References,
                true,
                UInteger.valueOf(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
                UInteger.valueOf(BrowseResultMask.All.getValue())
        );
        BrowseResult browseResult = client.browse(browse).get();
        ReferenceDescription[] references = browseResult.getReferences();
        String tempKey;
        for (ReferenceDescription reference : references) {
            tempKey = reference.getNodeId().getIdentifier().toString();

            if (tempKey.indexOf("._Hints") > 0 || tempKey.indexOf("._System") > 0 || tempKey.indexOf("._Statistics") > 0) {
                continue;
            }
//            System.out.println(tempKey);
            if (tempKey.indexOf(identifierPre) >= 0) {
                keys.add(tempKey);
            }

            if (reference.getNodeClass().getValue() == NodeClass.Object.getValue()) {
                filter(namespaceIndex, reference.getNodeId().getIdentifier().toString(), keys, client);
            }
        }
        return keys;
    }


    /**
     * 方法描述: 读取单个节点值
     *
     * @param namespaceIndex 命名空间
     * @param identifier
     * @param client
     * @return
     * @throws Exception
     */
    public static Object readValue(Integer namespaceIndex, String identifier, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        NodeId nodeId = new NodeId(namespaceIndex, identifier);
        DataValue value = null;

        value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();

        if (Objects.nonNull(value) && Objects.nonNull(value.getValue()) && Objects.nonNull(value.getValue().getValue())) {
            return value.getValue().getValue();
        }
        return null;
    }

    /**
     * @param namespaceIndex 命名空间
     * @param keys           点位集合
     * @param client         客户端
     * @return {@link List<DataValue>}
     * @throws Exception
     */
    public static List<DataValue> readValues(Integer namespaceIndex, List<String> keys, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        List<NodeId> nodeIdList = new ArrayList<>(500);
        keys.forEach(e -> {
            NodeId nodeId = new NodeId(namespaceIndex, e);
            nodeIdList.add(nodeId);
        });
        List<DataValue> dataValues = client.readValues(0.0, TimestampsToReturn.Both, nodeIdList).get();
        return dataValues;
    }


    /**
     * 写入值
     *
     * @param namespaceIndex 命名空间
     * @param identifier
     * @param value
     * @param client
     * @throws Exception
     */
    public static void writeValue(Integer namespaceIndex, String identifier, Object value, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        //创建变量节点
        NodeId nodeId = new NodeId(namespaceIndex, identifier);
        //创建Variant对象和DataValue对象
        Variant v = new Variant(value);
        DataValue dataValue = new DataValue(v, null, null);
        StatusCode statusCode = client.writeValue(nodeId, dataValue).get();
        if (statusCode.isGood()) {
            log.info("******写入成功 {} = {}", identifier, value);
        } else {
            log.error("******写入失败 {} = {}", identifier, value);
        }
    }

    /**
     * 写入值
     *
     * @param namespaceIndex
     * @param identifier
     * @param value
     * @param client
     * @throws Exception
     */
    public static void writeValueShortFinal(Integer namespaceIndex, String identifier, Object value, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        int loopTimes = 0;
        writeValue(namespaceIndex, identifier, value, client);
        Object tempValue;
        while (loopTimes < 3) {
            tempValue = readValue(namespaceIndex, identifier, client);
            if (tempValue != null && value.toString().equals(tempValue.toString())) {
                break;
            }
            writeValue(namespaceIndex, identifier, value, client);
            loopTimes++;
        }
    }

    /**
     * 方法描述:  写入多个节点的值
     *
     * @param namespaceIndex 命名空间
     * @param keys           节点集合
     * @param values         值集合
     * @param client         客户端
     * @return {@link Object}
     * @throws
     */
    public static Object writeValues(Integer namespaceIndex, Set<String> keys, List<Object> values, OpcUaClient client) throws Exception {
        if (client == null) {
            throw new RuntimeException("连接客户端不能为空");
        }
        List<NodeId> nodeIs = new ArrayList<>(keys.size());
        keys.forEach(e -> {
            NodeId nodeId = new NodeId(namespaceIndex, e);
            nodeIs.add(nodeId);
        });
        List<DataValue> dataValues = new ArrayList<>(values.size());
        values.forEach(e -> {
            Variant value = new Variant(Double.parseDouble(e.toString()));
            DataValue dataValue = new DataValue(value);
            dataValues.add(dataValue);
        });
        client.writeValues(nodeIs, dataValues).get();
        return null;
    }


    /**
     * 方法描述: 断开连接
     *
     * @param client
     * @return
     */
    public static void disconnect(OpcUaClient client) {
        if (client == null) {
            return;
        }
        try {
            client.disconnect().get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}


